#include "configuration.h"

#include "algorithms/LCSAlgorithm.h"
#include "algorithms/LevenshteinAlgorithm.h"
#include "algorithms/MD4Algorithm.h"
#include "algorithms/MD5Algorithm.h"
#include "algorithms/img/HistComparisonAlgorithm.h"

#include "Filter.h"
#include "EqualityFilter.h"
#include "SizeFilter.h"
#include "FormatFilter.h"
#include "FormatQualifier.h"

#include <string>
#include <fstream>
#include <sstream>
#include <memory>

static const std::string DEFAULT_CONFIG_FILE_NAME = "config";
static const std::string DEFAULT_HASH_ALG = "md5";
static const std::string DEFAULT_APPROX_ALG = "lcs";
static const std::string DEFAULT_IMG_ALG = "histogram";

static const double DEFAULT_SIMILARITY_THRESHOLD = 0.3;
static const double DEFAULT_SIZE_THRESHOLD = 1.0;
static const size_t DEFAULT_THREAD_COUNT = 10;

static const bool DEFAULT_TSV_ENABLED = false;

boost::program_options::options_description get_keys() {

    po::options_description keys_description("Supported options");
    keys_description.add_options()
        ("root,r", po::value<std::string>(), 
            "specifies root folder for dedupe (necessary)")

        ("hash-alg,f", po::value<std::string>(), 
            "sets hashing algorithm")

        ("approx-alg,a", po::value<std::string>(), 
            "sets approximation algorithm")

        ("img-alg,i", po::value<std::string>(),
            "sets image comparison algorithm")

        ("similarity-threshold,s", po::value<double>(), 
            "sets the threshold for two files similarity")

        ("tsv,t", "specifies tab-separated-values format for the output table")

        ("size-threshold,d", po::value<double>(), 
            "sets maximum size difference between comparable files")

        ("config,c", po::value<std::string>(), 
            "config file name, by default config file name is \"config\"")
        
        ("thread-count,n", po::value<size_t>(), "number of working threads")

        ("help,h", "shows help information");

    return keys_description; 
}

SettingsType get_settings(const std::string& config_file_name) {

    SettingsType settings;

    std::ifstream config(config_file_name.c_str());
    if (!config) {
        throw std::invalid_argument("Config file not found.");
    }

    std::stringstream content;
    content << config.rdbuf();
    config.close();     

    std::string s = content.str();
    ConfigParser parser;

    if (!parser.parse(s, settings.first, settings.second)) {
        throw std::invalid_argument("Parse error has been occured.");
    }

    return settings;
}

std::string get_config_file_name(const po::variables_map& options) {
    if (options.count("config")) {
        return options["config"].as<std::string>();
    }
    return DEFAULT_CONFIG_FILE_NAME;
}

std::string get_hash_alg_name(const po::variables_map& options) {
    if (options.count("hash-alg")) {
        return options["hash-alg"].as<std::string>();
    }
    return DEFAULT_HASH_ALG;
}

std::string get_approx_alg_name(const po::variables_map& options) {
    if (options.count("approx-alg")) {
        return options["approx-alg"].as<std::string>();
    }
    return DEFAULT_APPROX_ALG;
}

std::string get_img_comparison_alg_name(const po::variables_map& options) {
    if (options.count("img-alg")) {
        return options["img-alg"].as<std::string>();
    }
    return DEFAULT_IMG_ALG;
}

double get_size_threshold(const po::variables_map& options,
                          const std::map<std::string, std::string>& variables) {

    if (options.count("size-threshold")) {
        return options["size-threshold"].as<double>();
    }
    if (variables.find("size_threshold") == variables.end()) {
        return DEFAULT_SIZE_THRESHOLD;
    }
    return std::stod(variables.at("size_threshold"));
}

double get_similarity_threshold(const po::variables_map& options,
       const std::map<std::string, std::string>& variables) {

    if (options.count("similarity-threshold")) {
        return options["similarity-threshold"].as<double>();
    }
    if (variables.find("similarity_threshold") == variables.end()) {
        return DEFAULT_SIMILARITY_THRESHOLD;
    }
    return std::stod(variables.at("similarity_threshold"));
}

size_t get_thread_count(const po::variables_map& options,
                        const std::map<std::string, std::string>& variables) {

    if (options.count("thread-count")) {
        return options["thread-count"].as<size_t>();
    }
    if (variables.find("thread_count") == variables.end()) {
        return DEFAULT_THREAD_COUNT;
    }
    return size_t(std::stoi(variables.at("thread_count")));
}

std::shared_ptr<HashAlgorithm> get_hash_alg(const po::variables_map& options) {

    std::string name = get_hash_alg_name(options);

    if (name == "md4") {
        return std::shared_ptr<HashAlgorithm>(new MD4Algorithm());
    }
    if (name == "md5") {
        return std::shared_ptr<HashAlgorithm>(new MD5Algorithm());
    }
    throw std::invalid_argument("Invalid name of the hashing function.");
}

std::shared_ptr<ApproxAlgorithm> get_approx_alg(const po::variables_map& options,
                                 const std::map<std::string, std::string>& variables) { 

    std::string name = get_approx_alg_name(options);
    double threshold = get_similarity_threshold(options, variables);

    if (name == "lcs") {
        return std::shared_ptr<ApproxAlgorithm>(new LCSAlgorithm(threshold));
    }
    if (name == "levenshtein") {
        return std::shared_ptr<ApproxAlgorithm>(new LevenshteinAlgorithm(threshold));
    }
    throw std::invalid_argument("Invalid name of the approximation algorithm.");
}

std::shared_ptr<ApproxAlgorithm> get_img_comparison_alg(const po::variables_map& options,
                                 const std::map<std::string, std::string>& variables) {
    std::string name = get_img_comparison_alg_name(options);

    if (name == "histogram") {
        return std::shared_ptr<ApproxAlgorithm>(new HistComparisonAlgorithm);
    }
    throw std::invalid_argument("Invalid name of the image comparison algorithm.");
}

std::shared_ptr<SelectorAlgorithm> get_selector_alg(const po::variables_map& options,
                                   const std::map<std::string, std::string>& variables) {
    return std::shared_ptr<SelectorAlgorithm>(
           new SelectorAlgorithm(
               get_approx_alg(options, variables),
               get_img_comparison_alg(options, variables)
               )
           );
}

std::shared_ptr<FormatQualifier> get_format_qualifier(const std::map<std::string, std::string>& formats) {

    return std::shared_ptr<FormatQualifier>(new FormatQualifier(formats));
}

std::shared_ptr<FilesystemReader> get_filesystem_reader(const po::variables_map& options,
                                  const std::map<std::string, std::string>& formats) {

    return std::shared_ptr<FilesystemReader>(
           new FilesystemReader(
               options["root"].as<std::string>(), 
               get_hash_alg(options),
               get_format_qualifier(formats)
               )
           );
}

CompositeFilter get_filter(const po::variables_map& options,
                           const std::map<std::string, std::string>& variables,
                           const std::map<std::string, std::string>& formats) {

    CompositeFilter filter;

    auto size_threshold = get_size_threshold(options, variables);

    std::shared_ptr<Filter> eq_filter(new EqualityFilter());
    std::shared_ptr<Filter> size_filter(new SizeFilter(size_threshold));
    std::shared_ptr<Filter> format_filter(new FormatFilter(formats));

    filter.addFilter(eq_filter);
    filter.addFilter(size_filter);
    filter.addFilter(format_filter);

    return filter;
}

std::shared_ptr<Printer> get_printer(const po::variables_map& options) {
    
    return std::shared_ptr<Printer>(new Printer(options.count("tsv")));
}

std::shared_ptr<TaskLauncher> get_task_launcher(const po::variables_map& options,
                              const std::map<std::string, std::string>& variables) {

    return std::shared_ptr<TaskLauncher>(
           new TaskLauncher(get_thread_count(options, variables))
           );
}

std::shared_ptr<Executor> get_executor(const po::variables_map& options,
                      const std::map<std::string, std::string>& variables) {

    return std::shared_ptr<Executor>(
           new Executor(
               get_task_launcher(options, variables),
               get_printer(options) 
               )
           ); 
}
            
std::shared_ptr<FileDedupe> get_file_dedupe(const po::variables_map& options,
                           const std::map<std::string, std::string>& variables,
                           const std::map<std::string, std::string>& formats) {

    return std::shared_ptr<FileDedupe>(
           new FileDedupe(
               get_filesystem_reader(options, formats),
               get_filter(options, variables, formats),
               get_selector_alg(options, variables),
               get_executor(options, variables)
               )
           );
}


